using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using YoutubeExplode.Exceptions;
using YoutubeExplode.Utils.Extensions;

namespace YoutubeExplode.Videos.ClosedCaptions;

/// <summary>
/// Operations related to closed captions of YouTube videos.
/// </summary>
public class ClosedCaptionClient(HttpClient http)
{
    private readonly ClosedCaptionController _controller = new(http);

    private async IAsyncEnumerable<ClosedCaptionTrackInfo> GetClosedCaptionTrackInfosAsync(
        VideoId videoId,
        [EnumeratorCancellation] CancellationToken cancellationToken = default
    )
    {
        var playerResponse = await _controller.GetPlayerResponseAsync(videoId, cancellationToken);

        foreach (var trackData in playerResponse.ClosedCaptionTracks)
        {
            var url =
                trackData.Url
                ?? throw new YoutubeExplodeException("Failed to extract the track URL.");

            var languageCode =
                trackData.LanguageCode
                ?? throw new YoutubeExplodeException("Failed to extract the track language code.");

            var languageName =
                trackData.LanguageName
                ?? throw new YoutubeExplodeException("Failed to extract the track language name.");

            yield return new ClosedCaptionTrackInfo(
                url,
                new Language(languageCode, languageName),
                trackData.IsAutoGenerated
            );
        }
    }

    /// <summary>
    /// Gets the manifest that lists available closed caption tracks for the specified video.
    /// </summary>
    public async ValueTask<ClosedCaptionManifest> GetManifestAsync(
        VideoId videoId,
        CancellationToken cancellationToken = default
    ) => new(await GetClosedCaptionTrackInfosAsync(videoId, cancellationToken));

    private async IAsyncEnumerable<ClosedCaption> GetClosedCaptionsAsync(
        ClosedCaptionTrackInfo trackInfo,
        [EnumeratorCancellation] CancellationToken cancellationToken = default
    )
    {
        var response = await _controller.GetClosedCaptionTrackResponseAsync(
            trackInfo.Url,
            cancellationToken
        );

        foreach (var captionData in response.Captions)
        {
            var text = captionData.Text;

            // Skip over empty captions, but not captions containing only whitespace
            // https://github.com/Tyrrrz/YoutubeExplode/issues/671
            if (string.IsNullOrEmpty(text))
                continue;

            // Auto-generated captions may be missing offset or duration
            // https://github.com/Tyrrrz/YoutubeExplode/discussions/619
            if (captionData.Offset is not { } offset || captionData.Duration is not { } duration)
            {
                continue;
            }

            var parts = new List<ClosedCaptionPart>();
            foreach (var partData in captionData.Parts)
            {
                var partText = partData.Text;

                // Skip over empty parts, but not parts containing only whitespace
                // https://github.com/Tyrrrz/YoutubeExplode/issues/671
                if (string.IsNullOrEmpty(partText))
                    continue;

                var partOffset =
                    partData.Offset
                    ?? throw new YoutubeExplodeException(
                        "Failed to extract the caption part offset."
                    );

                var part = new ClosedCaptionPart(partText, partOffset);

                parts.Add(part);
            }

            yield return new ClosedCaption(text, offset, duration, parts);
        }
    }

    /// <summary>
    /// Gets the closed caption track identified by the specified metadata.
    /// </summary>
    public async ValueTask<ClosedCaptionTrack> GetAsync(
        ClosedCaptionTrackInfo trackInfo,
        CancellationToken cancellationToken = default
    ) => new(await GetClosedCaptionsAsync(trackInfo, cancellationToken));

    /// <summary>
    /// Writes the closed caption track identified by the specified metadata to the specified writer.
    /// </summary>
    /// <remarks>
    /// Closed captions are written in the SRT file format.
    /// </remarks>
    public async ValueTask WriteToAsync(
        ClosedCaptionTrackInfo trackInfo,
        TextWriter writer,
        IProgress<double>? progress = null,
        CancellationToken cancellationToken = default
    )
    {
        static string FormatTimestamp(TimeSpan value) =>
            Math.Floor(value.TotalHours).ToString("00", CultureInfo.InvariantCulture)
            + ':'
            + value.Minutes.ToString("00", CultureInfo.InvariantCulture)
            + ':'
            + value.Seconds.ToString("00", CultureInfo.InvariantCulture)
            + ','
            + value.Milliseconds.ToString("000", CultureInfo.InvariantCulture);

        // Would be better to use GetClosedCaptionsAsync(...) instead for streaming,
        // but we need the total number of captions to report progress.
        var track = await GetAsync(trackInfo, cancellationToken);

        var buffer = new StringBuilder();
        foreach (var (i, caption) in track.Captions.Index())
        {
            cancellationToken.ThrowIfCancellationRequested();

            buffer
                // Line number
                .AppendLine((i + 1).ToString(CultureInfo.InvariantCulture))
                // Time start --> time end
                .Append(FormatTimestamp(caption.Offset))
                .Append(" --> ")
                .Append(FormatTimestamp(caption.Offset + caption.Duration))
                .AppendLine()
                // Content
                .AppendLine(
                    caption.Text
                    // Caption text may contain valid SRT-formatted data in itself.
                    // This can happen, for example, if the subtitles for a YouTube video
                    // were imported from an SRT file, but something went wrong in the
                    // process, resulting in parts of the file being read as captions
                    // rather than control sequences.
                    // SRT file format does not provide any means of escaping special
                    // characters, so as a workaround we just replace the dashes in the
                    // arrow sequence with en-dashes, which look similar enough.
                    // https://github.com/Tyrrrz/YoutubeExplode/issues/755
                    .Replace("-->", "––>", StringComparison.Ordinal)
                );

            await writer.WriteLineAsync(buffer.ToString());
            buffer.Clear();

            progress?.Report((i + 1.0) / track.Captions.Count);
        }
    }

    /// <summary>
    /// Downloads the closed caption track identified by the specified metadata to the specified file.
    /// </summary>
    /// <remarks>
    /// Closed captions are written in the SRT file format.
    /// </remarks>
    public async ValueTask DownloadAsync(
        ClosedCaptionTrackInfo trackInfo,
        string filePath,
        IProgress<double>? progress = null,
        CancellationToken cancellationToken = default
    )
    {
        using var writer = File.CreateText(filePath);
        await WriteToAsync(trackInfo, writer, progress, cancellationToken);
    }
}
